runner: report per-query pool_peak_bytes in the result comment - #19
Merged
Conversation
apache/datafusion#23985 added `pool_peak_bytes` to each `BenchQuery` in the benchmark results JSON: the high-water mark of `MemoryPool::reserved()` for that query. `bench.sh compare_detail` cannot show it — `compare.py`'s `QueryResult` parses `elapsed` and `row_count` and nothing else — so the field reaches the results file and stops there. Read the same `results/<side>/*.json` here instead. DataFusion keeps the measurement; this repo keeps the presentation. The comment gains a `Memory Pool Peaks` section with a per-query base-vs-changed table, plus the largest peak in each run paired against the peak RSS `monitor.rs` sampled for the same invocation. The pairing is per benchmark invocation, not per query: the pool figure is per query while the RSS figure covers the whole run, so a per-query ratio against a run-wide RSS would not mean anything. Even the per-benchmark gap is labelled as an upper bound on unaccounted allocation rather than a measurement of it — the two high-water marks need not coincide in time, and RSS also covers data loading and allocator retention. Measurement only. No thresholds, no gates, nothing asserting a relationship between pool accounting and real allocation. Results file names don't follow benchmark names (`tpch` writes `tpch_sf1.json`, `topk_tpch` writes `run_topk_tpch.json`), so each invocation is bracketed with a directory snapshot and whatever JSON appears is attributed to it. The field is absent whenever a side ran without `DATAFUSION_RUNTIME_MEMORY_LIMIT` — the default — or predates #23985, e.g. `baseline: ref: v45.0.0`. Missing is rendered as `n/a` with a note naming the side, never as zero. When neither side recorded anything the whole section is omitted, so default runs post the comment they posted before. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Surfaces DataFusion's per-query
pool_peak_bytesin the result comment, for both sides.apache/datafusion#23985 (merged 2026-07-30) added
PeakRecordingPool, a delegatingMemoryPoolwrapper that records the high-water mark ofMemoryPool::reserved(), and gave eachBenchQueryin the results JSON apool_peak_bytesfield. Nothing reads it yet: the DataFusion path here delegates per-query comparison to./bench.sh compare_detail, andcompare.py'sQueryResultparseselapsedandrow_countand nothing else. The field reaches the results file and stops there.So this reads
results/<side>/*.jsonitself, in a newrunner/pool_peak.rs. DataFusion keeps the measurement; this repo keeps the presentation.Why a separate section rather than extra columns
That's forced rather than chosen. The existing per-query table is
compare.pyoutput, arriving here as opaque text inside a code fence — adding columns to it means patching DataFusion, which is the boundary this change is deliberately staying on the near side of.The comment gains a
Memory Pool Peaks<details>block between the wall-time report and Resource Usage, holding both the per-query table and the RSS pairing. Those stay together because the pairing is a max over the per-query numbers directly above it, and is hard to read without them.Why the RSS pairing is per benchmark, not per query
pool_peak_bytesis scoped to one query;monitor.rssamples the process subtree across a whole benchmark invocation. A per-query ratio against a run-wide RSS would not mean anything, so the only pairing offered is per invocation: the largest per-query peak in a run against that run's peak RSS.Even that is labelled conservatively. Both numbers are high-water marks over the same window, but they need not occur at the same instant, and RSS additionally covers data loading and allocator retention. The section says the gap is an upper bound on what the pool did not account for, not a measurement of it.
Measurement only
No thresholds, no gates, no CI check on the ratio. apache/datafusion#22626 and its fix #22742 were reverted wholesale by #22860 for asserting a relationship between pool accounting and real allocation that DataFusion does not claim — operators deliberately leave in-flight batches untracked. #23985 got in by being measurement only; this keeps that property.
Degrading when the field isn't there
Two ways it can be absent, indistinguishable from the JSON alone, so the note names both:
DATAFUSION_RUNTIME_MEMORY_LIMITin the trigger comment, per-side or shared.baseline: ref: v45.0.0againstmain— a normal request.Missing renders as
n/awith a note naming the side, never as zero. A recorded0renders as0 Band is never conflated with missing. When neither side recorded anything the section is omitted entirely, so a default run posts the comment it posted before.Attribution
Results file names don't follow benchmark names (
tpchwritestpch_sf1.json,topk_tpchwritesrun_topk_tpch.json,h2owrites one per run type), so there's nothing to match on. Each invocation is bracketed with a directory snapshot and whatever JSON appears is attributed to it.Criterion targets produce no results JSON and are absent from the section.
Testing
14 new tests, biased toward the awkward cases rather than the happy path: field absent on one side, absent on both, present with a zero value, a query in one side's JSON but not the other, one benchmark writing several results files, unparseable and non-JSON files in the results directory, missing resource stats, and RSS sampling in under the pool peak.
cargo fmt,cargo build,cargo clippy --all-targets --all-features -- -D warningsall clean; 142 tests pass.Beyond the unit tests,
dfbenchwas built at #23985's merge commit (f398301) and both cases were run for real, then put throughcollect_new→format_pool_peak_section— the same code path the runner uses:dfbench nlj --query 1 -i 2 --memory-limit 512Memits"pool_peak_bytes": 262528, which parses toSome(262528)and renders as256.4 KiB.--memory-limitemits query keys['query', 'iterations', 'start_time', 'success']— nopool_peak_bytes— which parses toNoneand renders asn/awith the note naming the side.The renderer was also exercised against a real saved
clickbench_partitioned.json(43 queries, pre-#23985 shape,"Query 0"-style string ids).What was not verified: the full runner path —
/workspaceclones, K8s, and GitHub — was not exercised, so the snapshot/attribution logic has only been tested against synthetic results directories rather than a livebench.shrun. A real end-to-end run is worth doing before relying on this.Follow-on, not in this change
A companion
alloc_peak(peak live heap bytes) is specified in the original handoff via a trackingGlobalAlloc. That predates a cheaper option — arrow-rs'sTrackingMemoryPoolplusclaim()throughArrowMemoryPool, which is shared-buffer-accurate where aGlobalAlloccounter is not. As of 2026-07-29 no operator callsclaim()and apache/datafusion#22898 has no picked direction, so the allocator route isn't obsolete, just no longer the default. Worth evaluating both once this has produced real numbers.🤖 Generated with Claude Code